package org.budo.launcher.tomcat;

import java.io.File;
import java.io.InputStream;
import java.util.HashSet;

import javax.servlet.jsp.JspFactory;

import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.Service;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardThreadExecutor;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.StandardRoot;
import org.apache.jasper.runtime.JspFactoryImpl;
import org.apache.tomcat.websocket.server.WsSci;
import org.budo.launcher.service.AbstractBudoLauncherService;
import org.budo.launcher.service.util.LauncherUtil;
import org.budo.support.lang.util.ArrayUtil;
import org.budo.support.lang.util.IoUtil;
import org.budo.support.lang.util.ReflectUtil;
import org.budo.support.slf4j.Slf4j;
import org.slf4j.Logger;

/**
 * Tomcat 启动器
 * 
 * @author lmw
 * @see org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer
 * @see org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory
 * @see https://github.com/nutzam/nutzboot/blob/dev/nutzboot-starter/nutzboot-starter-tomcat/src/main/java/org/nutz/boot/starter/tomcat/TomcatStarter.java
 */
public class BudoTomcatLauncher extends AbstractBudoLauncherService {
    private static final Logger log = Slf4j.getLogger();

    private static final int SESSION_TIMEOUT = 30;

    private static final String WEB_SOCKET_SERVER_CONTAINER_INITIALIZER = "org.apache.tomcat.websocket.server.WsSci";

    private static final String HOST = "0.0.0.0";

    private static final String PORT_KEY = "tomcat.port";

    public static final String DEFAULT_PROTOCOL = "org.apache.coyote.http11.Http11NioProtocol";

    @Override
    public void run(String[] args) {
        try {
            this.startTomcatServer();
        } catch (Throwable e) {
            log.error("#48 startTomcatServer error", e);
            throw new RuntimeException(e);
        }
    }

    private void startTomcatServer() throws Throwable {
        Tomcat tomcat = new Tomcat();

        File tempDir = LauncherUtil.createTempDir("BudoTomcatLauncherBaseDir", this.getLauncherPort(PORT_KEY));
        String tempPath = tempDir.getAbsolutePath();

        log.info("#51 tempPath=" + tempPath);
        tomcat.setBaseDir(tempPath);

        tomcat.setPort(this.getLauncherPort(PORT_KEY));
        tomcat.setHostname(HOST);

        Connector connector = new Connector(DEFAULT_PROTOCOL);
        connector.setPort(this.getLauncherPort(PORT_KEY));

        Service service = tomcat.getService();
        service.addConnector(connector);

        StandardThreadExecutor threadExecutor = new StandardThreadExecutor();
        connector.getService().addExecutor(threadExecutor);
        tomcat.setConnector(connector);

        Host host = tomcat.getHost();
        host.setAutoDeploy(false);

        Engine engine = tomcat.getEngine();
        engine.setBackgroundProcessorDelay(30);

        StandardContext tomcatContext = this.tomcatContext();

        this.addDefaultServlet(tomcatContext);

        this.addJspServlet(tomcatContext);

        this.parseWebXml(tomcatContext);

        // 默认 index
        this.addDefaultWelcomeFiles(tomcatContext);

        this.initWebsocketContainer(tomcatContext);

        // add tomcatContext
        host.addChild(tomcatContext);

//        String contextPath = this.getContextPath();
//        if (null != contextPath && !contextPath.isEmpty() && !contextPath.equals("/")) {
//            StandardContext child = new StandardContext();
//            child.setName("BudoIndex");
//            host.addChild(child);
//        }

        tomcat.start();
        log.info("#72 started, tomcat=" + tomcat + ", port=" + this.getLauncherPort(PORT_KEY));

        // 维持
        tomcat.getServer().await();
    }

    /**
     * 设置默认的 WelcomeFile
     */
    private void addDefaultWelcomeFiles(StandardContext tomcatContext) {
        String[] welcomeFiles = tomcatContext.getWelcomeFiles();
        for (String each : DEFAULT_WELCOME_FILES) {
            if (!ArrayUtil.contains(welcomeFiles, each)) {
                tomcatContext.addWelcomeFile(each);
            }
        }
    }

    /**
     * parse web.xml
     */
    private void parseWebXml(StandardContext tomcatContext) {
        File webXmlFile = new File(tomcatContext.getDocBase(), "WEB-INF/web.xml");
        if (!webXmlFile.exists()) {
            return;
        }

        InputStream inputStream = IoUtil.toInputStream(webXmlFile);
        TomcatWebXmlConfigParser webXmlConfigParser = new TomcatWebXmlConfigParser(inputStream, tomcatContext);
        webXmlConfigParser.parseWebXml();
    }

    private void initWebsocketContainer(StandardContext tomcatContext) {
        // websocket init before start
        if (ReflectUtil.hasClass(WEB_SOCKET_SERVER_CONTAINER_INITIALIZER)) {
            log.warn("#89 websocket init before start, tomcatContext=" + tomcatContext);
            tomcatContext.addServletContainerInitializer(new WsSci(), new HashSet<Class<?>>());
        }
    }

    private StandardContext tomcatContext() throws Throwable {
        File documentRoot = LauncherUtil.getDocumentRoot();
        log.info("#109 documentRoot=" + documentRoot);

        String rootPath = null;
        if (null != documentRoot) {
            rootPath = documentRoot.getCanonicalPath(); // 本地
        }

        if (null == rootPath) {
            File webapp = new File("webapp");
            if (webapp.exists() && webapp.isDirectory()) {
                rootPath = webapp.getCanonicalPath(); // 服务器

                log.info("#121 rootPath=" + rootPath);
            }
        }

        StandardContext tomcatContext = new StandardContext();

        String contextPath = this.getContextPath();
        tomcatContext.setPath(contextPath); // ContextPath

        tomcatContext.setDocBase(rootPath);
        tomcatContext.setName("BudoTomcatLauncherContext");
        tomcatContext.setDelegate(false);
        tomcatContext.addLifecycleListener(new Tomcat.FixContextListener());

        tomcatContext.setParentClassLoader(this.getClass().getClassLoader());

        tomcatContext.setSessionTimeout(SESSION_TIMEOUT); // 单位分钟

        StandardRoot standardRoot = new StandardRoot(tomcatContext);
        tomcatContext.setResources(standardRoot);

        File workDir = LauncherUtil.createTempDir("BudoTomcatLauncherTomcatContextWorkDir", this.getLauncherPort(PORT_KEY));
        String workDirPath = workDir.getAbsolutePath();

        log.info("#103 workDirPath=" + workDirPath);
        tomcatContext.setWorkDir(workDirPath);

        return tomcatContext;
    }

    private void addJspServlet(StandardContext context) {
        if (JspFactory.getDefaultFactory() == null) {
            JspFactory.setDefaultFactory(new JspFactoryImpl());
        }

        Wrapper jspServlet = context.createWrapper();
        jspServlet.setName("JspServlet");
        jspServlet.setServletClass("org.apache.jasper.servlet.JspServlet");
        jspServlet.setLoadOnStartup(1);
        jspServlet.setOverridable(true);

        context.addChild(jspServlet);
        this.addServletMapping(context, "*.jsp", "JspServlet");

        log.info("#161 addJspServlet, context=" + context + ", jspServlet=" + jspServlet);
    }

    @SuppressWarnings("deprecation")
    private void addServletMapping(StandardContext context, String pattern, String name) {
        context.addServletMapping(pattern, name);
    }

    private void addDefaultServlet(StandardContext context) {
        Wrapper defaultServlet = context.createWrapper();
        defaultServlet.setName("default");
        defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet");
        defaultServlet.addInitParameter("debug", "0");
        defaultServlet.addInitParameter("listings", "false");
        defaultServlet.setLoadOnStartup(1);
        defaultServlet.setOverridable(true);

        context.addChild(defaultServlet);
        this.addServletMapping(context, "/", "default");

        log.info("#176 addDefaultServlet, context=" + context + ", defaultServlet=" + defaultServlet);
    }
}